package com.han.swagger2markdown.core;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.han.swagger2markdown.core.Lists;
import io.swagger.models.*;
import io.swagger.models.parameters.*;
import io.swagger.models.properties.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.service.Documentation;
import springfox.documentation.spring.web.DocumentationCache;
import springfox.documentation.swagger2.mappers.ServiceModelToSwagger2Mapper;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;

@RestController
@RequestMapping("/docs")
@AllArgsConstructor
public class DocsController {

    private final List<String> stack = new ArrayList<>();

    private final DocumentationCache documentationCache;
    private final ServiceModelToSwagger2Mapper mapper;

    @GetMapping("/markdown")
    public synchronized void markdown(HttpServletResponse response) {
        Properties properties = new Properties();
        properties.put("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
        Velocity.init(properties);
        VelocityContext context = this.createVelocityContext();

        response.setContentType("text/markdown;charset=UTF-8");
        response.addHeader("Content-Disposition", "attachment;filename=docs.md");

        try (PrintWriter writer = response.getWriter()) {
            Template template = Velocity.getTemplate("docs.md.vm", "UTF-8");
            template.merge(context, writer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private VelocityContext createVelocityContext() {
        Documentation documentation = this.documentationCache.documentationByGroup("default");
        Swagger swagger = this.mapper.mapDocumentation(documentation);

        // controller list
        List<Tag> tags = swagger.getTags().stream().filter(t -> !"docs-controller".equals(t.getName())).collect(toList());
        Map<String, Path> paths = swagger.getPaths();

        Map<String, List<Api>> docsMap = tags.stream().collect(toMap(Tag::getName, tag -> {
            List<Api> list = new ArrayList<>();
            for (String uri : paths.keySet()) {
                List<OperationWrapper> opOperations = getOpOperation(paths.get(uri));
                if (opOperations.isEmpty() || !tag.getName().equals(opOperations.get(0).getOperation().getTags().get(0))) {
                    continue;
                }

                list.addAll(opOperations.stream().map(op -> buildApi(uri, op, swagger)).collect(toList()));
            }
            return list;
        }));

        VelocityContext context = new VelocityContext();
        context.put("docsMap", docsMap);
        return context;
    }

    private Api buildApi(String uri, OperationWrapper wrapper, Swagger swagger) {
        Operation operation = wrapper.getOperation();

        return Api.builder()
                .name(operation.getSummary())
                .uri(uri)
                .type(wrapper.getType())
                .requests(createRequests(operation, swagger))
                .response(createResponse(operation, swagger))
                .build();
    }

    private String createResponse(Operation operation, Swagger swagger) {
        Map<String, Model> definitions = swagger.getDefinitions();

        Response response = operation.getResponses().get("200");
        Map<String, Object> properties = buildProperties(definitions, ((RefModel) response.getResponseSchema()).getSimpleRef());

        // sort: code msg result
        Map<String, Object> linkedHashMap = new LinkedHashMap<>();
        linkedHashMap.put("code", properties.get("code"));
        linkedHashMap.put("msg", properties.get("msg"));

        if (properties.containsKey("result")) {
            linkedHashMap.put("result", properties.get("result"));
        }

        return prettyFormatJson(linkedHashMap);
    }

    private List<Request> createRequests(Operation operation, Swagger swagger) {
        Map<String, Model> definitions = swagger.getDefinitions();

        List<Request> list = new ArrayList<>();
        Request body = null; // body at the back

        for (Parameter p : operation.getParameters()) {
            Request request = new Request();
            request.setName(p.getName());
            request.setParamType(p.getIn());
            request.setRequired(p.getRequired());
            request.setRemark(p.getDescription());

            if (p instanceof HeaderParameter) {
                HeaderParameter hp = (HeaderParameter) p;
                request.setType(hp.getType());
                list.add(request);
                continue;
            }

            if (p instanceof QueryParameter) {
                QueryParameter qp = (QueryParameter) p;
                request.setType(qp.getType());
                list.add(request);
                continue;
            }

            if (p instanceof PathParameter) {
                PathParameter pp = (PathParameter) p;
                request.setType(pp.getType());
                list.add(request);
                continue;
            }

            if (p instanceof BodyParameter && ((BodyParameter) p).getSchema() instanceof RefModel) {
                RefModel model = (RefModel) ((BodyParameter) p).getSchema();
                Map<String, Object> properties = buildProperties(definitions, model.getSimpleRef());
                request.setBody(prettyFormatJson(properties));
                body = request;
                continue;
            }

            if (p instanceof BodyParameter && ((BodyParameter) p).getSchema() instanceof ArrayModel) {
                Property items = ((ArrayModel) ((BodyParameter) p).getSchema()).getItems();
                if (items instanceof RefProperty) {
                    List<Map<String, Object>> list1 = Lists.newArrayList(buildProperties(definitions, ((RefProperty) items).getSimpleRef()));
                    request.setBody(prettyFormatJson(list1));
                } else {
                    request.setBody(prettyFormatJson(defaultStringList()));
                }
                body = request;
            }
        }

        list.sort(Comparator.comparing(Request::getParamType));

        if (body != null) {
            list.add(body);
        }
        return list;
    }

    private Map<String, Object> buildProperties(Map<String, Model> definitions, String simpleRef) {
        stack.add(simpleRef);
        Map<String, Property> properties = definitions.get(simpleRef).getProperties();

        Map<String, Object> collect = properties.values().stream()
                .collect(toMap(Property::getName, p -> determineDescription(p, definitions), (p1, p2) -> p2));

        stack.remove(stack.size() - 1);
        return collect;
    }

    private Object determineDescription(Property property, Map<String, Model> definitions) {
        if (property instanceof IntegerProperty) {
            return property.getDescription() == null ? "integer" : property.getDescription();
        }

        if (property instanceof LongProperty) {
            return new Date().getTime();
        }

        if (property instanceof BooleanProperty) {
            return false;
        }

        if (property instanceof StringProperty) {
            String enums = ((StringProperty) property).getEnum() == null ? "" : " " + ((StringProperty) property).getEnum().toString();
            String description = property.getDescription() == null ? "string" : property.getDescription();
            return description + enums;
        }

        if (property instanceof ObjectProperty || property instanceof MapProperty) {
            return property.getDescription() == null ? new HashMap<>() : property.getDescription();
        }

        if (property instanceof RefProperty) {
            String simpleRef = ((RefProperty) property).getSimpleRef();
            if (stack.contains(simpleRef)) {
                return new HashMap<>();
            }
            return buildProperties(definitions, simpleRef);
        }

        if (property instanceof ArrayProperty) {
            Property items = ((ArrayProperty) property).getItems();
            if (items instanceof StringProperty) {
                return defaultStringList();
            }
            Object description = determineDescription(items, definitions);
            return description instanceof HashMap && ((HashMap<?, ?>) description).isEmpty() ? Lists.newArrayList() : Lists.newArrayList(description);
        }

        return property.getDescription();
    }

    private List<OperationWrapper> getOpOperation(Path path) {
        List<OperationWrapper> list = new ArrayList<>();
        if (path.getGet() != null) {
            list.add(new OperationWrapper("GET", path.getGet()));
        }

        if (path.getPost() != null) {
            list.add(new OperationWrapper("POST", path.getPost()));
        }

        if (path.getPut() != null) {
            list.add(new OperationWrapper("PUT", path.getPut()));
        }

        if (path.getDelete() != null) {
            list.add(new OperationWrapper("DELETE", path.getDelete()));
        }

        if (path.getHead() != null) {
            list.add(new OperationWrapper("HEAD", path.getHead()));
        }

        if (path.getPatch() != null) {
            list.add(new OperationWrapper("PATCH", path.getPatch()));
        }

        if (path.getOptions() != null) {
            list.add(new OperationWrapper("OPTIONS", path.getOptions()));
        }
        return list;
    }

    private List<String> defaultStringList() {
        return Lists.newArrayList("string1", "string2", "string3");
    }

    private String prettyFormatJson(Object object) {
        return JSON.toJSONString(object, SerializerFeature.PrettyFormat, SerializerFeature.WriteMapNullValue,
                SerializerFeature.WriteDateUseDateFormat);
    }

    @Data
    @AllArgsConstructor
    public static class OperationWrapper {
        private String type;
        private Operation operation;
    }

    @Data
    @Builder
    public static class Api {
        private String name;
        private String uri;
        private String type;
        private List<Request> requests;
        private String response;
    }

    @Data
    public static class Request {
        private String name;
        private String type;
        private String paramType;
        private boolean required;
        private String remark;
        private String body;
    }
}
